﻿using System;
using System.Net;
using System.ServiceModel;

namespace Nardax.ServiceModel
{
    public sealed class ServiceProxy<TServiceContract> : IServiceProxy<TServiceContract> where TServiceContract : class
    {
        private readonly string _configurationFileName;
        private readonly object _channelFactoryLock;
        private ChannelFactory<TServiceContract> _channelFactory;
        private TServiceContract _channel;
        private readonly object _channelLock;
        private readonly string _username;
        private readonly string _password;
        private readonly string _domain;

        public ServiceProxy(TServiceContract channel)
            : this()
        {
            _channel = channel;
        }

        public ServiceProxy(string configurationFileName = null, string username = null, string password = null, string domain = null, string endpointName = "*")
        {
            _configurationFileName = configurationFileName;
            _domain = domain;
            _username = username;
            _password = password;
            EndpointName = endpointName;
            _channelFactoryLock = new object();
            _channelLock = new object();
        }

        public void Call(Action<TServiceContract> method)
        {
            var channel = GetOpenChannel();
            var success = false;

            try
            {
                method(channel);
                success = true;
            }
            finally
            {
                if (!success)
                {
                    Abort();
                }
            }
        }

        public TResult Call<TResult>(Func<TServiceContract, TResult> method)
        {
            var channel = GetOpenChannel();
            var success = false;

            try
            {
                var result = method(channel);
                success = true;
                return result;
            }
            finally
            {
                if (!success)
                {
                    Abort();
                }
            }
        }

        private TServiceContract GetOpenChannel()
        {
            if (_channel == null)
            {
                lock (_channelLock)
                {
                    if (_channel == null)
                    {
                        _channel = GetChannelFactory().CreateChannel();
                        ((IClientChannel)_channel).Open();
                    }
                }
            }

            return _channel;
        }

        public void Close()
        {
            lock (_channelLock)
            {
                if (_channel != null)
                {
                    ((IClientChannel)_channel).Close();
                    _channel = default(TServiceContract);
                }
            }
        }

        private void Abort()
        {
            lock (_channelLock)
            {
                if (_channel != null)
                {
                    ((IClientChannel)_channel).Abort();
                    _channel = default(TServiceContract);
                }
            }
        }

        private ChannelFactory<TServiceContract> GetChannelFactory()
        {
            if (_channelFactory == null)
            {
                // use a lock and an additional check on the key to protect against a raise condition when creating the factory
                lock (_channelFactoryLock)
                {
                    if (_channelFactory == null && !string.IsNullOrEmpty(_configurationFileName))
                    {
                        _channelFactory = new CustomChannelFactory<TServiceContract>(EndpointName, _configurationFileName);
                        TryAddUsernamePassword();
                    }
                    else if (_channelFactory == null)
                    {
                        _channelFactory = new ChannelFactory<TServiceContract>(EndpointName);
                        TryAddUsernamePassword();
                    }
                }
            }

            return _channelFactory;
        }

        private void TryAddUsernamePassword()
        {
            // if I have a domain name here make the assumption that we are working with windows authentication
            if (!string.IsNullOrEmpty(_domain) && !string.IsNullOrEmpty(_username) && !string.IsNullOrEmpty(_password))
            {
                _channelFactory.Credentials.Windows.ClientCredential = new NetworkCredential
                                                                           {
                                                                               Domain = _domain,
                                                                               UserName = _username,
                                                                               Password = _password
                                                                           };
            }

            else if (!string.IsNullOrEmpty(_username) && !string.IsNullOrEmpty(_password))
            {
                _channelFactory.Credentials.UserName.UserName = _username;
                _channelFactory.Credentials.UserName.Password = _password;
            }
        }

        public string EndpointName { get; set; }
    }
}
